#region

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Reflection;
using Charisma.Linq;
using Charisma.MongoDb.Linq.Expressions;
using System.Linq;

#endregion

namespace Charisma.MongoDb.Linq
{
    internal partial class Translator : QueryableVisitor
    {
        private readonly TranslatorContext context;
        internal TranslatorState state;

        public Translator(TranslatorContext context)
        {
            Contract.Requires(context != null);

            this.context = context;
            state = new TranslatorState(this);
        }

        protected override Expression VisitQueryableMethod(MethodCallExpression mc, QueryableMethodKind methodKind)
        {
            using (state.CreateScope())
            {
                switch (methodKind)
                {
                    case QueryableMethodKind.Cast:
                        return VisitCast(mc.Arguments[0], mc.Method.GetGenericArguments()[0],
                                         mc.Arguments[0].Type.GetGenericArguments()[0]);
                    case QueryableMethodKind.AsQueryable:
                        return VisitAsQueryable(mc.Arguments[0]);
                    case QueryableMethodKind.AsEnumerable:
                        break;
                    case QueryableMethodKind.ToArray:
                        break;
                    case QueryableMethodKind.ToList:
                        break;
                    case QueryableMethodKind.Aggregate:
                        break;
                    case QueryableMethodKind.ElementAt:
                        return VisitElementAt(mc.Arguments[0], mc.Arguments[1], context.IsRoot(mc), mc.Method.ReturnType,
                                              false);
                    case QueryableMethodKind.ElementAtOrDefault:
                        return VisitElementAt(mc.Arguments[0], mc.Arguments[1], context.IsRoot(mc), mc.Method.ReturnType,
                                              true);
                    case QueryableMethodKind.Last:
                        break;
                    case QueryableMethodKind.LastOrDefault:
                        break;
                    case QueryableMethodKind.Except:
                    case QueryableMethodKind.Intersect:
                    case QueryableMethodKind.Concat:
                    case QueryableMethodKind.Union:
                        return VisitSetOperations(mc.Arguments[0], mc.Arguments[1], methodKind);
                    case QueryableMethodKind.Reverse:
                        break;
                    case QueryableMethodKind.SequenceEqual:
                        break;
                    case QueryableMethodKind.DefaultIfEmpty:
                        break;
                    case QueryableMethodKind.SkipWhile:
                        break;
                    case QueryableMethodKind.TakeWhile:
                        break;
                    case QueryableMethodKind.All:
                        if (mc.Arguments.Count == 2)
                            return VisitAll(mc.Arguments[0], mc.Arguments[1].StripQuotes(), context.IsRoot(mc));
                        break;
                    case QueryableMethodKind.OfType:
                        return VisitOfType(mc.Arguments[0], mc.Method.GetGenericArguments()[0],
                                           mc.Arguments[0].Type.GetGenericArguments()[0]);
                    case QueryableMethodKind.Any:
                        if (mc.Arguments.Count == 1)
                            return VisitAny(mc.Arguments[0], null, context.IsRoot(mc));
                        if (mc.Arguments.Count == 2)
                            return VisitAny(mc.Arguments[0], mc.Arguments[1].StripQuotes(), context.IsRoot(mc));
                        break;
                    case QueryableMethodKind.Contains:
                        if (mc.Arguments.Count == 2)
                            return VisitContains(mc.Arguments[0], mc.Arguments[1], context.IsRoot(mc));
                        break;
                    case QueryableMethodKind.Distinct:
                        if (mc.Arguments.Count == 1)
                            return VisitDistinct(mc.Arguments[0]);
                        break;
                    case QueryableMethodKind.First:
                    case QueryableMethodKind.FirstOrDefault:
                    case QueryableMethodKind.Single:
                    case QueryableMethodKind.SingleOrDefault:
                        if (mc.Arguments.Count == 1)
                            return VisitFirstSingle(mc.Arguments[0], null, mc.Method, context.IsRoot(mc));
                        if (mc.Arguments.Count == 2)
                        {
                            var predicate = (mc.Arguments[1].StripQuotes());
                            return VisitFirstSingle(mc.Arguments[0], predicate, mc.Method, context.IsRoot(mc));
                        }
                        break;
                    case QueryableMethodKind.GroupBy:
                        if (mc.Arguments.Count == 2)
                        {
                            return VisitGroupBy(
                                mc.Method.ReturnType,
                                mc.Arguments[0],
                                mc.Arguments[1].StripQuotes(),
                                null,
                                null
                                );
                        }
                        if (mc.Arguments.Count == 3)
                        {
                            var lambda1 = mc.Arguments[1].StripQuotes();
                            var lambda2 = mc.Arguments[2].StripQuotes();
                            if (lambda2.Parameters.Count == 1)
                            {
                                // second lambda is element selector
                                return VisitGroupBy(
                                    mc.Method.ReturnType,
                                    mc.Arguments[0],
                                    lambda1,
                                    lambda2,
                                    null);
                            }
                            if (lambda2.Parameters.Count == 2)
                            {
                                // second lambda is result selector
                                return VisitGroupBy(
                                    mc.Method.ReturnType,
                                    mc.Arguments[0],
                                    lambda1,
                                    null,
                                    lambda2);
                            }
                        }
                        else if (mc.Arguments.Count == 4)
                        {
                            return VisitGroupBy(
                                mc.Method.ReturnType,
                                mc.Arguments[0],
                                mc.Arguments[1].StripQuotes(),
                                mc.Arguments[2].StripQuotes(),
                                mc.Arguments[3].StripQuotes()
                                );
                        }
                        break;
                    case QueryableMethodKind.GroupJoin:
                        return VisitGroupJoin(mc.Arguments[0],
                                              mc.Arguments[1],
                                              mc.Arguments[2].StripQuotes(),
                                              mc.Arguments[3].StripQuotes(),
                                              mc.Arguments[4].StripQuotes(),
                                              mc.Arguments.Count > 5 ? mc.Arguments[5] : null,
                                              mc);
                    case QueryableMethodKind.Join:
                        return VisitJoin(mc.Arguments[0],
                                         mc.Arguments[1],
                                         mc.Arguments[2].StripQuotes(),
                                         mc.Arguments[3].StripQuotes(),
                                         mc.Arguments[4].StripQuotes(),
                                         false,
                                         mc);
                    case QueryableMethodKind.OrderBy:
                        return VisitOrderBy(mc.Arguments[0], mc.Arguments[1].StripQuotes(), true);
                    case QueryableMethodKind.OrderByDescending:
                        return VisitOrderBy(mc.Arguments[0], mc.Arguments[1].StripQuotes(), false);
                    case QueryableMethodKind.Select:
                        return VisitSelect(mc.Arguments[0], mc.Arguments[1].StripQuotes());
                    case QueryableMethodKind.SelectMany:
                        if (mc.Arguments.Count == 2)
                        {
                            return VisitSelectMany(mc.Arguments[0],
                                                   mc.Arguments[1].StripQuotes(),
                                                   null,
                                                   mc);
                        }
                        if (mc.Arguments.Count == 3)
                        {
                            return VisitSelectMany(mc.Arguments[0],
                                                   mc.Arguments[1].StripQuotes(),
                                                   mc.Arguments[2].StripQuotes(),
                                                   mc);
                        }
                        break;
                    case QueryableMethodKind.LongCount:
                    case QueryableMethodKind.Count:
                    case QueryableMethodKind.Max:
                    case QueryableMethodKind.Min:
                    case QueryableMethodKind.Sum:
                    case QueryableMethodKind.Average:
                        if (mc.Arguments.Count == 1)
                            return VisitAggregate(mc.Arguments[0], mc.Method, null, context.IsRoot(mc), mc);
                        if (mc.Arguments.Count == 2)
                            return VisitAggregate(mc.Arguments[0], mc.Method, mc.Arguments[1].StripQuotes(), context.IsRoot(mc), mc);
                        break;
                    case QueryableMethodKind.Skip:
                        if (mc.Arguments.Count == 2)
                            return VisitSkip(mc.Arguments[0], mc.Arguments[1]);
                        break;
                    case QueryableMethodKind.Take:
                        if (mc.Arguments.Count == 2)
                            return VisitTake(mc.Arguments[0], mc.Arguments[1]);
                        break;
                    case QueryableMethodKind.ThenBy:
                        return VisitThenBy(mc.Arguments[0], mc.Arguments[1].StripQuotes(), true);
                    case QueryableMethodKind.ThenByDescending:
                        return VisitThenBy(mc.Arguments[0], mc.Arguments[1].StripQuotes(), false);
                    case QueryableMethodKind.Where:
                        return VisitWhere(mc.Arguments[0], mc.Arguments[1].StripQuotes());
                    default:
                        throw new ArgumentOutOfRangeException("methodKind");
                }
            }
            throw new NotSupportedException(
                String.Format("Unable to translate expression '{0}'. Linq translator does not support method {1}.", mc,
                              methodKind));
        }

        private ProjectionExpression VisitWhere(Expression expression, LambdaExpression le)
        {
            var sequence = VisitSequence(expression);
            using (context.Bindings.PermanentAdd(le.Parameters[0], sequence))
            using (state.CreateScope())
            {
                var filter = Visit(le.Body);
                var sourceQuery = sequence.Query;
                var query = sourceQuery.SetFilter(filter);
                return new ProjectionExpression(query, sequence.Projector, sequence.Aggregator, sequence.ResultType);
            }
        }

        private ProjectionExpression VisitTake(Expression expression, Expression count)
        {
            var sequence = VisitSequence(expression);
            var constCount = (int)ExpressionEvaluator.Evaluate(count).Value;
            var sourceQuery = sequence.Query;
            var query = sourceQuery.SetTake(constCount);
            return new ProjectionExpression(query, sequence.Projector, sequence.Aggregator, sequence.ResultType);
        }

        private ProjectionExpression VisitSkip(Expression expression, Expression count)
        {
            var sequence = VisitSequence(expression);
            var constCount = (int)ExpressionEvaluator.Evaluate(count).Value;
            var sourceQuery = sequence.Query;
            var query = sourceQuery.SetSkip(constCount);
            return new ProjectionExpression(query, sequence.Projector, sequence.Aggregator, sequence.ResultType);
        }

        private ProjectionExpression VisitAggregate(
            Expression source, 
            MethodInfo method, 
            LambdaExpression argument,
            bool isRoot, 
            MethodCallExpression mc)
        {
            AggregateKind aggregateKind;
            switch (method.Name)
            {
                case "Count":
                    aggregateKind = AggregateKind.Count;
                    break;
                case "LongCount":
                    aggregateKind = AggregateKind.Count;
                    break;
                case "Min":
                    aggregateKind = AggregateKind.Min;
                    break;
                case "Max":
                    aggregateKind = AggregateKind.Max;
                    break;
                case "Sum":
                    aggregateKind = AggregateKind.Sum;
                    break;
                case "Average":
                    aggregateKind = AggregateKind.Average;
                    break;
                default:
                    throw new NotSupportedException();
            }
            ProjectionExpression innerProjection;
            if (aggregateKind == AggregateKind.Count)
            {
                innerProjection = argument != null 
                    ? VisitWhere(source, argument) 
                    : VisitSequence(source);
            }
            else
            {
                innerProjection = VisitSequence(source);
                if (argument == null)
                {
                    if (innerProjection.Query.Fields.Count > 1)
                        throw new NotSupportedException();
                }
                else
                {
                    using (context.Bindings.Add(argument.Parameters[0], innerProjection))
                    using (state.CreateScope())
                    {
                    }
                }
            }


            throw new NotImplementedException();
        }

        private ProjectionExpression VisitSelectMany(Expression expression, LambdaExpression collectionSelector,
                                           LambdaExpression resultSelector, MethodCallExpression mc)
        {
            throw new NotImplementedException();
        }

        private ProjectionExpression VisitSelect(Expression expression, LambdaExpression le)
        {
            var sequence = VisitSequence(expression);
            using (context.Bindings.PermanentAdd(le.Parameters[0], sequence))
            using (state.CreateScope())
            {
                state.BuildingProjection = true;
                var itemProjector = Visit(le.Body);
                var fields = FieldExtractor.ExtractFields(itemProjector);
                var sourceQuery = sequence.Query.SetFields(fields);
                return new ProjectionExpression(sourceQuery, itemProjector, sequence.Aggregator, sequence.ResultType);
            }
        }

        private ProjectionExpression VisitOrderBy(Expression expression, LambdaExpression le, bool ascending)
        {
            var sequence = VisitSequence(expression);
            using (context.Bindings.PermanentAdd(le.Parameters[0], sequence))
            using (state.CreateScope())
            {
                var fieldsSelector = Visit(le.Body);
                var fields = ExtractFields(fieldsSelector);
                if (fields.Count == 0)
                    throw new InvalidOperationException();
                var sourceQuery = sequence.Query;
                var query = sourceQuery.SetOrder(fields
                    .Select(f => new OrderExpression(f, ascending))
                    .ToList());
                return new ProjectionExpression(query, sequence.Projector, sequence.Aggregator, sequence.ResultType);
            }
        }

        private ProjectionExpression VisitThenBy(Expression expression, LambdaExpression le, bool ascending)
        {
            var sequence = VisitSequence(expression);
            using (context.Bindings.PermanentAdd(le.Parameters[0], sequence))
            using (state.CreateScope())
            {
                var fieldsSelector = Visit(le.Body);
                var fields = ExtractFields(fieldsSelector);
                if (fields.Count == 0)
                    throw new InvalidOperationException();
                var sourceQuery = sequence.Query;
                var query = sourceQuery.AppendOrder(fields
                    .Select(f => new OrderExpression(f, ascending))
                    .ToList());
                return new ProjectionExpression(query, sequence.Projector, sequence.Aggregator, sequence.ResultType);
            }
        }

        private static ICollection<FieldExpression> ExtractFields(Expression e)
        {
            var result = new List<FieldExpression>();
            var expression = e.StripCasts();
            var field = expression as FieldExpression;
            if (field != null)
            {
                result.Add(field);
            }
            else if (expression.IsAnonymousConstructor())
            {
                var newExpression = (NewExpression)expression;
                var fieldExpressions = newExpression.Arguments
                    .OfType<FieldExpression>()
                    .ToList();
                if (newExpression.Arguments.Count != fieldExpressions.Count)
                    throw new InvalidOperationException();
                result.AddRange(fieldExpressions);
            }
            return result.AsReadOnly();
        }

        private ProjectionExpression VisitJoin(Expression left, Expression right, LambdaExpression leftSelector,
                                     LambdaExpression rightSelector, LambdaExpression resultSelector, bool isLeftJoin,
                                     MethodCallExpression mc)
        {
            throw new NotImplementedException();
        }

        private ProjectionExpression VisitGroupJoin(Expression left, Expression right, LambdaExpression leftSelector,
                                          LambdaExpression rightSelector, LambdaExpression elementSelector,
                                          Expression keyComparer, MethodCallExpression mc)
        {
            throw new NotImplementedException();
        }

        private ProjectionExpression VisitGroupBy(Type returnType, Expression expression, LambdaExpression projection,
                                        LambdaExpression elementSelector, LambdaExpression resultSelector)
        {
            throw new NotImplementedException();
        }

        private ProjectionExpression VisitFirstSingle(Expression source, LambdaExpression predicate, MethodInfo method, bool isRoot)
        {
            var resultType = ResultType.All;
            var projection = predicate != null
                    ? VisitWhere(source, predicate)
                    : VisitSequence(source);
            QueryExpression query;
            switch (method.Name)
            {
                case "First":
                    resultType = ResultType.First;
                    query = projection.Query.SetTake(1);
                    break;
                case "FirstOrDefault":
                    resultType = ResultType.FirstOrDefault;
                    query = projection.Query.SetTake(1);
                    break;
                case "Single":
                    resultType = ResultType.Single;
                    query = projection.Query.SetTake(2);
                    break;
                case "SingleOrDefault":
                    resultType = ResultType.SingleOrDefault;
                    query = projection.Query.SetTake(2);
                    break;
                default:
                    throw new NotSupportedException();
            }
            return new ProjectionExpression(query, projection.Projector, projection.Aggregator, resultType);
        }

        private Expression VisitDistinct(Expression expression)
        {
            var projection = VisitSequence(expression);
            var sourceQuery = projection.Query.SetDistinct();
            return new ProjectionExpression(sourceQuery, projection.Projector, projection.Aggregator, projection.ResultType);
        }

        private Expression VisitContains(Expression expression, Expression element, bool isRoot)
        {
            throw new NotImplementedException();
        }

        private Expression VisitAny(Expression expression, Expression predicate, bool isRoot)
        {
            throw new NotImplementedException();
        }

        private Expression VisitOfType(Expression expression, Type getGenericArgument, Type type)
        {
            throw new NotImplementedException();
        }

        private Expression VisitAll(Expression expression, LambdaExpression predicate, bool isRoot)
        {
            throw new NotImplementedException();
        }

        private Expression VisitSetOperations(Expression left, Expression right, QueryableMethodKind methodKind)
        {
            throw new NotImplementedException();
        }

        private Expression VisitElementAt(Expression p0, Expression p1, bool isRoot, Type returnType, bool ascending)
        {
            throw new NotImplementedException();
        }

        private Expression VisitAsQueryable(Expression expression)
        {
            throw new NotImplementedException();
        }

        private Expression VisitCast(Expression expression, Type getGenericArgument, Type type)
        {
            throw new NotImplementedException();
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(context != null);
            Contract.Invariant(state != null);
        }
    }
}